Completed
Push — master ( dfac26...6f374a )
by Jeff
08:12
created

Field.display   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
c 6
b 0
f 0
dl 0
loc 21
rs 8.7624
cc 5
nc 5
nop 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A 0 3 1
1
/** global: updateScreenUrl */
2
3
/**
4
 * Screen class constructor
5
 * @param {string} updateScreenUrl global screen update checks url
6
 */
7
function Screen(updateScreenUrl) {
8
  this.fields = [];
9
  this.url = updateScreenUrl;
10
  this.lastChanges = null;
11
  this.endAt = null;
12
  this.nextUrl = null;
13
  this.stopping = false;
14
  this.cache = {};
15
}
16
17
/**
18
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
19
 */
20
Screen.prototype.checkUpdates = function() {
21
  var s = this;
22
  $.get(this.url, function(j) {
23
    if (j.success) {
24
      if (s.lastChanges == null) {
25
        s.lastChanges = j.data.lastChanges;
26
      } else if (s.lastChanges != j.data.lastChanges) {
27
        s.reload();
28
        s.nextUrl = null;
29
        return;
30
      }
31
32
      if (j.data.duration > 0) {
33
        // Setup next screen
34
        s.reload(j.data.duration * 1000);
35
        s.nextUrl = j.data.nextScreenUrl;
36
      }
37
    } else if (j.message == 'Unauthorized') {
38
      screen.reload();
39
    }
40
  });
41
}
42
43
/**
44
 * Start Screen reload procedure, checking for every field timeout
45
 */
46
Screen.prototype.reload = function(minDuration) {
47
  var endAt = Date.now() + (minDuration ? minDuration : 0);
48
  if (this.stopping && this.endAt < endAt) {
49
    return;
50
  }
51
52
  this.endAt = minDuration ? Date.now() + minDuration : 0;
53
  this.stopping = true;
54
  for (var i in this.fields) {
55
    if (!this.fields.hasOwnProperty(i)) {
56
      continue;
57
    }
58
    var f = this.fields[i];
59
    if (f.timeout && f.endAt > this.endAt) {
60
      this.endAt = f.endAt;
61
    }
62
  }
63
64
  if (this.endAt === 0) {
65
    this.doReload();
66
  }
67
}
68
69
/**
70
 * Actual Screen reload action
71
 */
72
Screen.prototype.doReload = function() {
73
  if (this.nextUrl) {
74
    window.location = this.nextUrl;
75
  } else {
76
    window.location.reload();
77
  }
78
}
79
80
/**
81
 * Check every field for content
82
 * @param  {Content} data 
83
 * @return {boolean} content is displayed
84
 */
85
Screen.prototype.displaysData = function(data) {
86
  return this.fields.filter(function(field) {
87
    return field.current && field.current.data == data;
88
  }).length > 0;
89
}
90
91
/**
92
 * Content class constructor
93
 * @param {array} c content attributes
94
 */
95
function Content(c) {
96
  this.id = c.id;
97
  this.data = c.data;
98
  this.duration = c.duration * 1000;
99
  this.type = c.type;
100
  this.src = null;
101
102
  if (this.shouldPreload()) {
103
    this.preload();
104
  }
105
}
106
107
Content.preload = {
108
  PRELOADING: -2,
109
  PRELOADING_QUEUE: -3,
110
  HTTP_FAIL: -3,
111
  NO_EXPIRE_HEADER: -4,
112
  OK: -8,
113
}
114
115
116
/**
117
 * Check if content should be ajax preloaded
118
 * @return {boolean}
119
 */
120
Content.prototype.shouldPreload = function() {
121
  return this.canPreload() && !this.isPreloading() && !this.isPreloaded();
122
}
123
124
/**
125
 * Check if content has pre-loadable material
126
 * @return {boolean} 
127
 */
128
Content.prototype.canPreload = function() {
129
  return this.getResource() && this.type.search(/Video|Image|Agenda/) != -1;
130
}
131
132
/**
133
 * Extract url from contant data
134
 * @return {string} resource url
135
 */
136
Content.prototype.getResource = function() {
137
  if (this.src) {
138
    return this.src;
139
  }
140
  var srcMatch = this.data.match(/src="([^"]+)"/);
141
  if (!srcMatch) {
142
    return false;
143
  }
144
  var src = srcMatch[1];
145
  if (src.indexOf('/') === 0) {
146
    src = window.location.origin + src;
147
  }
148
  if (src.indexOf('http') !== 0) {
149
    return false;
150
  }
151
  src = src.replace(/#.*/g, '');
152
153
  this.src = src;
154
  return src;
155
}
156
157
/**
158
 * Set content cache status
159
 * @param {string} expires header
160
 */
161
Content.prototype.setPreloadState = function(expires) {
162
  if (expires === null || expires == '') {
163
    expires = Content.preload.NO_EXPIRE_HEADER;
164
  }
165
166
  screen.cache[this.getResource()] = expires < -1 ? expires : Content.preload.OK
167
}
168
169
/**
170
 * Check cache for preload status of content
171
 * @return {Boolean} 
172
 */
173
Content.prototype.isPreloaded = function() {
174
  if (!this.canPreload()) {
175
    return true;
176
  }
177
178
  return screen.cache[this.getResource()] === Content.preload.OK
179
}
180
181
/**
182
 * Check cache for in progress preloading
183
 * @return {Boolean}
184
 */
185
Content.prototype.isPreloading = function() {
186
  return screen.cache[this.getResource()] === Content.preload.PRELOADING;
187
}
188
189
/**
190
 * Ajax call to preload content
191
 * @return {[type]} [description]
192
 */
193
Content.prototype.preload = function() {
194
  var src = this.getResource();
195
  if (!src) {
196
    return;
197
  }
198
  this.setPreloadState(Content.preload.PRELOADING);
199
200
  var c = this;
201
  $.ajax(src).done(function(data, textStatus, jqXHR) {
202
    c.setPreloadState(jqXHR.getResponseHeader('Expires'));
203
  }).fail(function() {
204
    c.setPreloadState(Content.preload.FAIL);
205
  });
206
}
207
208
Content.prototype.canDisplay = function(screen) {
209
  return (screen.endAt == null || this.duration + Date.now() > screen.endAt) && this.isPreloaded();
210
}
211
212
/**
213
 * Field class constructor
214
 * @param {jQuery.Object} $f field object
215
 */
216
function Field($f) {
217
  this.$field = $f;
218
  this.id = $f.attr('data-id');
219
  this.url = $f.attr('data-url');
220
  this.types = $f.attr('data-types').split(' ');
221
  this.canUpdate = this.url != null;
222
  this.contents = [];
223
  this.previous = null;
224
  this.current = null;
225
  this.next = null;
226
  this.timeout = null;
227
  this.endAt = null;
228
}
229
230
/**
231
 * Retrieves contents from backend for this field
232
 */
233
Field.prototype.getContents = function() {
234
  if (!this.canUpdate) {
235
    return;
236
  }
237
238
  var f = this;
239
  $.get(this.url, function(j) {
240
    if (j.success) {
241
      f.contents = j.next.map(function(c) {
242
        return new Content(c);
243
      });
244
      if (!f.timeout && f.contents.length) {
245
        f.pickNext();
246
      }
247
    } else {
248
      f.setError(j.message || 'Error');
249
    }
250
  });
251
}
252
253
/**
254
 * Display error in field text
255
 */
256
Field.prototype.setError = function(err) {
257
  this.$field.text(err);
258
}
259
260
/**
261
 * Randomize order
262
 */
263
Field.prototype.randomizeSortContents = function() {
264
  this.contents = this.contents.sort(function() {
265
    return Math.random() - 0.5;
266
  });
267
}
268
269
/**
270
 * Loop through field contents to pick next displayable content
271
 */
272
Field.prototype.pickNext = function() {
273
  if (screen.stopping && screen.endAt < Date.now()) { // Stoping screen
274
    screen.doReload();
275
    return;
276
  }
277
278
  var f = this;
279
  this.previous = this.current;
280
  this.current = null;
281
  var pData = this.previous && this.previous.data;
282
  // Avoid repeat & other field same content
283
  this.randomizeSortContents();
284
  for (var i = 0; i < this.contents.length; i++) {
285
    var c = this.contents[i];
286
    // Skip too long or not preloaded content 
287
    if (!c.canDisplay(screen)) {
288
      continue;
289
    }
290
291
    if (c.data == pData) {
292
      // Will repeat, avoid if enough content
293
      if (this.contents.length < 2) {
294
        this.next = c;
295
        break;
296
      }
297
      continue;
298
    }
299
300
    if (screen.displaysData(c.data)) {
301
      // Same content already displayed on other field, avoid if enough content
302
      if (this.contents.length < 3) {
303
        this.next = c;
304
        break;
305
      }
306
      continue;
307
    }
308
309
    this.next = c;
310
    break
311
  }
312
313
  if (this.next) {
314
    this.display();
315
  } else {
316
    setTimeout(function() {
317
      f.pickNext();
318
    }, 200);
319
  }
320
}
321
322
/**
323
 * Display next content in field html
324
 */
325
Field.prototype.display = function() {
326
  var f = this;
327
  if (this.next && this.next.duration > 0) {
328
    this.current = this.next
329
    this.next = null;
330
    this.$field.html(this.current.data);
331
    this.$field.show();
332
    if (this.$field.text() != '') {
333
      this.$field.textfill({
334
        maxFontPixels: 0,
335
      });
336
    }
337
    if (this.timeout) {
338
      clearTimeout(this.timeout);
339
    }
340
    this.timeout = setTimeout(function() {
341
      f.pickNext();
342
    }, this.current.duration);
343
    this.endAt = this.current.duration + Date.now()
344
  }
345
}
346
347
/**
348
 * jQuery.load event
349
 * Initialize Screen and Fields
350
 * Setup updates interval timeouts
351
 */
352
var screen = null;
353
354
function onLoad() {
355
  screen = new Screen(updateScreenUrl);
356
  // Init
357
  $('.field').each(function() {
358
    var f = new Field($(this));
359
    f.getContents();
360
    screen.fields.push(f);
361
  });
362
363
  // Setup content updates loop
364
  setInterval(function() {
365
    for (var f in screen.fields) {
366
      if (screen.fields.hasOwnProperty(f)) {
367
        screen.fields[f].getContents();
368
      }
369
    }
370
    screen.checkUpdates();
371
  }, 60000);
372
  screen.checkUpdates();
373
}
374
375
// Run
376
$(onLoad);
377